<?php
require_once 'Zend/Controller/Action.php';
require_once 'Zend/Controller/Action/Helper/ContextSwitch.php';


class Rest_Controller_Action extends Zend_Controller_Action
{

	private static $_validPOSTs = array('PUT' , 'DELETE');

	private static $_validGETs = array('HEAD' , 'OPTIONS');

	private static $_restStarted = false;

	public $contexts = array(
		'list' => array('amf', 'json', 'xml', 'csv', 'tab'), 
		'view' => array('amf', 'json', 'xml'), 
		'create' => array('amf', 'json', 'xml'), 
		'update' => array('amf', 'json', 'xml'), 
		'delete' => array('amf', 'json', 'xml')
	);

	/**
	 * Initialize the RESTful framework by allowing a method override for
	 * browsers and other applications that can't utilize all request methods.
	 */
	public static function startRest ()
	{
		if (self::$_restStarted)
			return;
		
		$request = new Zend_Controller_Request_Http(); //Zend_Controller_Front::getInstance()->getRequest();
		$request = Zend_Controller_Front::getInstance()->getRequest();
		
		$override = $request->getHeader('x-method');
		if ($override == null)
			$override = $request->getParam('method');
		
		if ($override != null) {
			$method = $request->getMethod();
			if ($method == 'POST' && in_array($override, self::$_validPOSTs)) {
				$_SERVER['REQUEST_METHOD'] = $method;
			} elseif ($method == 'GET' && in_array($override, self::$_validGETs)) {
				$_SERVER['REQUEST_METHOD'] = $method;
			}
		}
		
		self::$_restStarted = true;
	}

	/**
	 * Class constructor
	 *
	 * The request and response objects should be registered with the
	 * controller, as should be any additional optional arguments; these will be
	 * available via {@link getRequest()}, {@link getResponse()}, and
	 * {@link getInvokeArgs()}, respectively.
	 *
	 * When overriding the constructor, please consider this usage as a best
	 * practice and ensure that each is registered appropriately; the easiest
	 * way to do so is to simply call parent::__construct($request, $response,
	 * $invokeArgs).
	 *
	 * After the request, response, and invokeArgs are set, the
	 * {@link $_helper helper broker} is initialized.
	 *
	 * Finally, {@link init()} is called as the final action of
	 * instantiation, and may be safely overridden to perform initialization
	 * tasks; as a general rule, override {@link init()} instead of the
	 * constructor to customize an action controller's instantiation.
	 *
	 * @param Zend_Controller_Request_Abstract $request
	 * @param Zend_Controller_Response_Abstract $response
	 * @param array $invokeArgs Any additional invocation arguments
	 * @return void
	 */
	public function __construct (Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
	{
		self::startRest();
		parent::__construct($request, $response, $invokeArgs);
	}

	public function init ()
	{
		$contextSwitch = new Zend_Controller_Action_Helper_ContextSwitch();
		$contextSwitch = $this->_helper->getHelper('contextSwitch');
		$contextSwitch->setContexts(array(
			'amf' => array(
				//'headers' => array('Content-Type' => 'application/x-amf'),
				'callbacks' => array(
					'init' => array($this, 'disableLayoutAndView') , 
					'post' => array($this, 'postAmfContext'))) , 
			'json' => array(
				'suffix' => 'json', 
				//'headers'   => array('Content-Type' => 'application/json'),
				'callbacks' => array(
					'init' => array($contextSwitch, 'initJsonContext'), 
					'post' => array($this, 'postJsonContext'))), 
			'xml' => array(
				'suffix' => 'xml', 
				'headers'   => array('Content-Type' => 'text/xml'),
				'callbacks' => array(
					'init' => array($this, 'disableLayoutAndView'), 
					'post' => array($this, 'postXmlContext'))),
			'csv' => array(
				'suffix' => 'csv', 
				'headers'   => array('Content-Type' => 'text/plain'),
				'callbacks' => array(
					'init' => array($this, 'disableLayoutAndView'), 
					'post' => array($this, 'postCsvContext'))),
			'tab' => array(
				'suffix' => 'csv', 
				'headers'   => array('Content-Type' => 'text/plain'),
				'callbacks' => array(
					'init' => array($this, 'disableLayoutAndView'), 
					'post' => array($this, 'postTabContext')))		   
			));
		$contextSwitch->initContext();
	}

	/**
	 * Check if the current action is valid for the request method present. If
	 * it is not, you should 'return' out of the action. This method will call
	 * _forward if an available action is present or will throw the appropriate
	 * HTTP status error. This check should be at the beginning of every RESTful
	 * method. If the action is not one of the default RESTful actions (list,
	 * view, create, update, or delete) the method to check against must be
	 * supplied or isActionValid will always return true. Examples:
	 * 
	 * public function viewAction()
	 * {
	 *	 if (!$this->isActionValid()) return;
	 *	 // .... code to 'view' item
	 * }
	 * 
	 * public function suspendAction()
	 * {
	 *	 if (!$this->isActionValid('PUT')) return;
	 *	 // .... code to 'suspend' item
	 * }
	 * 
	 * @param string $method The method which this action should check against.
	 * This is not needed for default RESTful actions: list, view, create,
	 * update, and delete.
	 * 
	 * @return boolean Whether the method is indeed valid. If not, you should
	 * immediately return out of the function.
	 */
	protected function isValidAction ($method = null)
	{
		$request = $this->getRequest();
		$action = $request->getActionName();
		
		if ($action == 'list') {
			if ($request->isPost()) {
				$this->_forward('create');
				return false;
			}
		} elseif ($action == 'view') {
			if ($request->isPut()) {
				$this->_forward('update');
				return false;
			} elseif ($request->isDelete()) {
				$this->_forward('delete');
				return false;
			}
		}
		
		if ($method && $method != $request->getMethod()) {
			throw new Zend_Controller_Action_Exception("Cannot do a RESTful $action without the $method method", 200);
		}
		
		return true;
	}

	/**
	 * Disables the layout and optionally view so that one may use their own
	 * rendering mechanism.
	 */
	public function disableLayoutAndView ($layoutOnly = false)
	{
		if (! $layoutOnly)
			$this->_helper->viewRenderer->setNoRender(true);
		if ($this->_helper->hasHelper('layout'))
			$this->_helper->layout->disableLayout();
	}

	/**
	 * Retrieves the data from the 
	 */
	public function getData ()
	{
		$context = $this->_helper->getHelper('contextSwitch')->getCurrentContext();
		$request = new Zend_Controller_Request_Http();
		$request = $this->getRequest();
		$method = $request->getMethod();
		$contents = $request->getRawBody();
		$data = null;
		
		if ($context == 'amf' && $contents && $contents[0] == "\n") {
			require_once 'Zend/Amf/Parse/InputStream.php';
			require_once 'Zend/Amf/Parse/Amf3/Deserializer.php';
			$stream = new Zend_Amf_Parse_InputStream(substr($contents, 1));
			$deserializer = new Zend_Amf_Parse_Amf3_Deserializer($stream);
			$data = $deserializer->readObject();
		} elseif ($context == 'json' && $contents && $contents[0] == "{") {
			require_once 'Zend/Json.php';
			$data = Zend_Json::decode($contents);
		} elseif ($context == 'xml' && $contents && $contents[0] == "<") {
			$data = simplexml_load_string($contents);
		} elseif ($method == 'POST' && $contents) {
			parse_str($contents, $data);
			$data = (object) $data;
		} elseif ($method == 'GET') {
			parse_str($request->getServer('QUERY_STRING'), $data);
			unset($data['format']);
			$data = (object) $data;
		}
		
		return $data;
	}

	/**
	 * Disables the layout and optionally view so that one may use their own
	 * rendering mechanism.
	 */
	public function postAmfContext ()
	{
		if ($this->view instanceof Zend_View_Interface) {
			require_once 'Zend/Amf/Parse/OutputStream.php';
			require_once 'Zend/Amf/Parse/Amf3/Serializer.php';
			$stream = new Zend_Amf_Parse_OutputStream();
			$serializer = new Zend_Amf_Parse_Amf3_Serializer($stream);
			if (is_string($this->view->data)) {
				$serializer->writeString($this->view->data);
			} else {
				$serializer->writeObject($this->view->data);
			}
			$this->getResponse()->setBody("\n" . $stream->getStream());
		}
	}

	public function postJsonContext ()
	{
		if (! $this->_helper->getHelper('contextSwitch')->getAutoJsonSerialization()) {
			return;
		}
		if ($this->view instanceof Zend_View_Interface) {
			require_once 'Zend/Json.php';
			$json = Zend_Json::encode($this->view->data);
			$this->getResponse()->setBody($json);
		}
	}

	public function postXmlContext ()
	{
		if ($this->view instanceof Zend_View_Interface) {
			require_once ('Rest/Util/XmlWriter.php');
			$writer = new Rest_Util_XmlWriter($this->view->data);
			$xml = $writer->getXML($this->view->data);
			$this->getResponse()->setBody($xml);
		}
	}
	
	public function postCsvContext ()
	{
		if ($this->view instanceof Zend_View_Interface) {
			require_once ('Rest/Util/CsvWriter.php');
			$writer = new Rest_Util_CsvWriter($this->view->data);
			$csv = $writer->getCSV(array($this->view->data));
			$this->getResponse()->setBody($csv);
		}
	}
	
	public function postTabContext ()
	{
		if ($this->view instanceof Zend_View_Interface) {
			require_once ('Rest/Util/CsvWriter.php');
			$writer = new Rest_Util_CsvWriter($this->view->data);
			$csv = $writer->getCSV(array($this->view->data), "\t");
			$this->getResponse()->setBody($csv);
		}
	}
}

